package com.device.config;

import com.device.authentication.DeviceClientAuthenticationConverter;
import com.device.authentication.DeviceClientAuthenticationProvider;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;

/**
 * @author chengyadong
 * @date 2024/2/28 15:13
 * @description
 */
@Configuration
public class AuthorizationServerConfig {

	private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";


	/**
	 * Spring Authorization Server 相关配置
	 * 此处方法与下面defaultSecurityFilterChain都是SecurityFilterChain配置，配置的内容有点区别，
	 * 因为Spring Authorization Server是建立在Spring Security 基础上的，defaultSecurityFilterChain方法主要
	 * 配置Spring Security相关的东西，而此处authorizationServerSecurityFilterChain方法主要配置OAuth 2.1和OpenID Connect 1.0相关的东西
	 */
	@Bean
	@Order(1)
	public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, RegisteredClientRepository registeredClientRepository,
			AuthorizationServerSettings authorizationServerSettings)
			throws Exception {
		OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

		//AuthenticationConverter(转换处理器)，尝试从HttpServletRequest提取客户端凭据,用以构建OAuth2ClientAuthenticationToken实例。
		DeviceClientAuthenticationConverter deviceClientAuthenticationConverter =
				new DeviceClientAuthenticationConverter(
						authorizationServerSettings.getDeviceAuthorizationEndpoint());
		//AuthenticationProvider(认证处理器)，用于验证OAuth2ClientAuthenticationToken。
		DeviceClientAuthenticationProvider deviceClientAuthenticationProvider =
				new DeviceClientAuthenticationProvider(registeredClientRepository);

		http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
				.deviceAuthorizationEndpoint(deviceAuthorizationEndpoint ->
						//设置用户码校验地址--即设备授权请求时返回的verification_uri，verification_uri_complete的相对地址，我们需要自定义该路径对应的方法信息
						//用户拿到verification_uri时直接访问肯定会被登录执行链进行登录处理，登录完成后我们需要自定义填写user_code页面等
						deviceAuthorizationEndpoint.verificationUri("/activate")
				)
				.deviceVerificationEndpoint(deviceVerificationEndpoint ->
						//设置授权页地址
						deviceVerificationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)
				)
				.clientAuthentication(clientAuthentication ->
						//将deviceClientAuthenticationConverter和deviceClientAuthenticationProvider设置到OAuth2ClientAuthenticationConfigurer中
						//进而初始化到了OAuth2ClientAuthenticationFilter(该过滤器会对/oauth2/device_authorization等路径拦截进行客户端认证处理)
						//但是需要注意/oauth2/device_authorization请求时只有client_id,scope参数，没有指定客户端认证方式，系统提供的4个转换和认证处理器是处理不了的，
						// none认证模式时也不行，其在转化的时候请求必须是授权码且含有code_verifier参数   所以此处指定了客户端请求转化处理器+客户端认证处理器
						clientAuthentication
								.authenticationConverter(deviceClientAuthenticationConverter)
								.authenticationProvider(deviceClientAuthenticationProvider)
				)
				.authorizationEndpoint(authorizationEndpoint ->
						authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))
				//开启OpenID Connect 1.0（其中oidc为OpenID Connect的缩写）。
				.oidc(Customizer.withDefaults());

		//设置登录地址，需要进行认证的请求被重定向到该地址
		http
				.exceptionHandling((exceptions) -> exceptions
						.defaultAuthenticationEntryPointFor(
								new LoginUrlAuthenticationEntryPoint("/login"),
								new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
						)
				)
				.oauth2ResourceServer(oauth2ResourceServer ->
						oauth2ResourceServer.jwt(Customizer.withDefaults()));

		return http.build();
	}


	/**
	 * 注册客户端 对应oauth2_registered_client表
	 *
	 * @param jdbcTemplate 操作数据库
	 * @return 客户端仓库
	 */
	@Bean
	public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
		// ---------- 1、检查当前客户端是否已注册
		// 操作数据库对象
		JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);

        /*
         客户端在数据库中的几个记录字段的说明
         ------------------------------------------
         id：仅表示客户端在数据库中的这个记录
         client_id：唯一标示客户端；请求token时，以此作为客户端的账号
         client_name：客户端的名称，可以省略
         client_secret：密码
         */
		String clientId_1 = "deviceClient";
		// 查询客户端是否存在
		RegisteredClient registeredClient_1 = registeredClientRepository.findByClientId(clientId_1);
		// ---------- 2、添加客户端
		// 数据库中没有
		if (registeredClient_1 == null) {
			registeredClient_1 = this.createRegisteredClientAuthorizationCode(clientId_1);
			registeredClientRepository.save(registeredClient_1);
		}
		// ---------- 3、返回客户端仓库
		return registeredClientRepository;
	}

	/**
	 * 定义客户端（令牌申请方式：授权码模式）
	 *
	 * @param clientId 客户端ID
	 * @return
	 */
	private RegisteredClient createRegisteredClientAuthorizationCode(final String clientId) {
		// JWT（Json Web Token）的配置项：TTL、是否复用refrechToken等等
		TokenSettings tokenSettings = TokenSettings.builder()
				// 令牌存活时间：2小时
				.accessTokenTimeToLive(Duration.ofHours(2))
				// 令牌可以刷新，重新获取
				.reuseRefreshTokens(true)
				// 刷新时间：30天（30天内当令牌过期时，可以用刷新令牌重新申请新令牌，不需要再认证）
				.refreshTokenTimeToLive(Duration.ofDays(30))
				// 决定accessToken采用哪种生成工具 OAuth2TokenFormat.SELF_CONTAINED:JwtGenerator(默认 idToken也是采用该生成器) 或者
				// OAuth2TokenFormat.REFERENCE:OAuth2AccessTokenGenerator配置的数据最终存入oauth2_registered_client中的token_settings中
				// 如果采用REFERENCE方式我们需要重新配置oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()))
				// 当前配置指定了资源端采用jwt的方式解析accessToken,会报错处理
				.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
				.build();
		// 客户端相关配置
		ClientSettings clientSettings = ClientSettings.builder()
				// 是否需要用户授权确认
				.requireAuthorizationConsent(true)
				//是否PKCE code请求中必须包含code_verifier字段
				//注意:一般在客户端的认证方式为none时,其默认会帮我们在请求中拼接PKCE需要的参数信息，所以该值为true或者false都无影响。但是再其他认证模式时该值如果为true时需要我们手动处理
				// 客户端的认证方式分为两大类：
				//(1)机密客户端：客户端会通过加密手段将client_secret传递到授权服务器从而进行认证，因此可以信任客户端的安全性，一般不需要使用PKCE
				//(2)公共客户端:当客户端不使用client_secret进行认证时，授权码授权流程可能容易受到中间人攻击，因为授权码可以在不安全的环境中传输。为了解决这个问题，PKCE引入了code_challenge和code_challenge_method参数，以确保即使授权码被中间人截获，也无法被滥用
				// 而none即为公共客户端模式，其他认证模式均为机密客户端模式。而springSecurity默认情况下如果我们的认证模式为none,会帮我们在授权请求参数中增加PKCE需要的参数，
				// 但是其他模式springSecurity是不会帮我们增加的，所以如果客户端认证模式非none，而此处又将PKCE模式开启就会出现参数不存在的异常

				//为了解决参数不存在的异常可以有两种方式：(1)机密客户端认证方式相对来说比较安全，我们可以将kpce配置直接关闭，设置为false
				//(2)机密客户端认证方式虽然很安全，但是我们仍然可以开启KPCE让他变得更安全，这就需要我们通过自定义手段再客户端发送授权请求前给其增加缺失的两个参数信息。
				// 我们可以自定义OAuth2AuthorizationRequestRedirectFilter过滤器中的authorizationRequestResolver属性，
				// 其本身默认为DefaultOAuth2AuthorizationRequestResolver，我们可以继承DefaultOAuth2AuthorizationRequestResolver，然后重定义里面的getBuilder方法即可
				.requireProofKey(false)
				.build();
		return RegisteredClient
				// 客户端ID和密码
				.withId(UUID.randomUUID().toString())
				//.withId(id)
				.clientId(clientId)
				//.clientSecret("{noop}123456")
				.clientSecret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"))
				// 客户端名称：可省略
				.clientName("deviceClientProject")
				// 授权方法
				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
				.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
				.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
				// ---------- 刷新令牌（授权码模式）
				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
				// ---------客户端模式
				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
				// ---------设备授权码模式
				.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
				/* 回调地址：
				 * 授权服务器向当前客户端响应时调用下面地址；
				 * 不在此列的地址将被拒绝；
				 * 只能使用IP或域名，不能使用localhost
				 */
				.redirectUri("")
				// 授权范围（当前客户端的授权范围）
				.scope("read")
				.scope("write")
				.scope("profile")
				.scope("openid")
				// JWT（Json Web Token）配置项
				.tokenSettings(tokenSettings)
				// 客户端配置项
				.clientSettings(clientSettings)
				.build();
	}

	/**
	 * 令牌的发放记录, 对应oauth2_authorization表
	 */
	@Bean
	public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
		return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
	}

	/**
	 * 把资源拥有者授权确认操作保存到数据库, 对应oauth2_authorization_consent表
	 */
	@Bean
	public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
		return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
	}

	// (1)JWT:JWT的本质就是一个字符串，它是将用户信息保存到一个Json字符串中，然后进行编码后得到一个JWT token，并且这个JWT token带有签名信息，接收后可以校验是否被篡改，
	// 所以可以用于在各方之间安全地将信息作为Json对象传输。JWT的具体实现可以分为以下几种：nonsecure JWT：未经过签名，不安全的JWT  JWS：经过签名的JWT
	// JWE：payload部分经过加密的JWT  我们通常使用的JWT一般都是JWS 缺点是无状态，一旦颁发除非超时不然一直有效 默认5分钟
	//步骤:1、用户登录 2、服务的认证，通过后根据secret生成token 3、将生成的token返回给浏览器 4、用户每次请求携带token
	//5、服务端利用公钥解读jwt签名，判断签名有效后，从Payload中获取用户信息 6、处理请求，返回响应结果
	// 这样请求头中带着该token，客户端拿到之后通过秘钥(密钥的获取一般两种方式：1在客户端配置加密时一模一样的秘钥和算法 2：客户端去授权端获取秘钥和算法)进行解密以获取用户信息
	// (2)因为jwt是无状态的，所以有时候我们可以采用Redis替换jwt redis原理：token：20位随机数通过base64加密形成的字符串  通过tokenStore(tokenStore)仅是注入redis实例，资源端是不需要存储token的
	// 优点：资源端拿到token可以按照一定的格式拼接出redis的key,通过一系列关联可以获取到用户信息，不需要远程调用授权服务器 ，减轻了授权服务器的压力
	// 缺点：增加了对redis组件的依赖 在往redis存储时会生成一些列的key,value值（大概9个）占用redis空间

	/**
	 * 加载JWT资源, 用于生成令牌,加密算法
	 * 参考：https://blog.csdn.net/gaoluan6052/article/details/127560759
	 */
	@Bean
	public JWKSource<SecurityContext> jwkSource() {
		KeyPair keyPair;
		try {
			// 创建KeyPairGenerator 指定算法为RSA，用于生成对应的公钥和私钥
			KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
			// 指定字节长度
			keyPairGenerator.initialize(2048);
			// 秘钥生成器
			keyPair = keyPairGenerator.generateKeyPair();
		} catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
		//公钥（public key），用于加密数据
		RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
		//私钥（private key），用于解密数据
		RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
		RSAKey rsaKey = new RSAKey.Builder(publicKey)
				.privateKey(privateKey)
				.keyID(UUID.randomUUID().toString())
				.build();
		//JWT的密钥或者密钥对，统一称为JSON Web Key，也就是JWK
		JWKSet jwkSet = new JWKSet(rsaKey);
		return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
	}

	/**
	 * JWT 解码
	 */
	@Bean
	public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
		return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
	}
/*	//自定义公钥解码
	@Bean
	public JwtDecoder jwtDecoder(){
		return NimbusJwtDecoder.withPublicKey("cheng").build();
	}*/

	/**
	 * AuthorizationServerSetting的相关配置(最终效果是将builder()方法中维护的路径键值对信息初始化到AbstractSettings中的settings集合中)
	 */
	@Bean
	public AuthorizationServerSettings authorizationServerSettings() {
		return AuthorizationServerSettings.builder().build();
	}


}
